﻿using PetaPoco;
using PetaPoco.Core;
using System.Runtime.CompilerServices;

namespace BlazorAdmin.Extensions
{
    /// <summary>
    /// 内部批次方法提供者实现类
    /// </summary>
    internal class BatchProvider
    {
        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="db">IDatabase 实例</param>
        public BatchProvider(IDatabase db) => Database = db;

        /// <summary>
        /// 获得 IDatabase 实例
        /// </summary>
        public IDatabase Database { get; }

        /// <summary>
        /// 获得/设置 每批实体大小，此值通过测试得出相对插入速度比较快的一个值 默认 50
        /// </summary>
        public int BatchSize { get; protected set; } = 50;

        /// <summary>
        /// 获得/设置 是否支持批量插入功能
        /// </summary>
        public bool SupportBulk { get; set; }

        /// <summary>
        /// 创建 BatchProvider 实例
        /// </summary>
        /// <param name="db">IDatabase 实例</param>
        /// <returns></returns>
        public static BatchProvider Create(IDatabase db)
        {
            return db.Provider.GetType().Name switch
            {
                "SqlServerDatabaseProvider" => new SqlServerBatchProvider(db),
                "SQLiteDatabaseProvider" => new SQLiteBatchProvider(db),
                "OracleDatabaseProvider" => new OracleBatchProvider(db),
                "PostgreSQLDatabaseProvider" => new PostgreSQLBatchProvider(db),
                _ => new BatchProvider(db),
            };
        }

        /// <summary>
        /// 批次插入方法
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="pocos"></param>
        public virtual void InsertBatch<T>(IEnumerable<T> pocos)
        {
            if (SupportBulk)
            {
                InsertBulk(pocos);
                return;
            }

            PocoData pd = PocoData.ForType(typeof(T), Database.DefaultMapper);
            InsertBatch(pd, pocos);
        }

        /// <summary>
        /// 批次插入方法
        /// </summary>
        /// <param name="tableName"></param>
        /// <param name="primaryKeyName"></param>
        /// <param name="autoIncrement"></param>
        /// <param name="pocos"></param>
        public virtual void InsertBatch(string tableName, string primaryKeyName, bool autoIncrement, IEnumerable<object> pocos)
        {
            if (SupportBulk)
            {
                InsertBulk(tableName, primaryKeyName, autoIncrement, pocos);
                return;
            }

            PocoData pocoData = PocoData.ForType(pocos.First().GetType(), Database.DefaultMapper);
            if (!string.IsNullOrEmpty(tableName))
            {
                pocoData.TableInfo.TableName = tableName;
            }

            if (!string.IsNullOrEmpty(primaryKeyName))
            {
                pocoData.TableInfo.PrimaryKey = primaryKeyName;
            }

            if (autoIncrement)
            {
                pocoData.TableInfo.AutoIncrement = autoIncrement;
            }

            InsertBatch(pocoData, pocos);
        }

        /// <summary>
        /// Bulk批次插入方法具体实现
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="pocos"></param>
        /// <exception cref="NotImplementedException"></exception>
        protected virtual void InsertBulk<T>(IEnumerable<T> pocos)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Bulk批次插入方法具体实现
        /// </summary>
        /// <param name="tableName"></param>
        /// <param name="primaryKeyName"></param>
        /// <param name="autoIncrement"></param>
        /// <param name="pocos"></param>
        /// <exception cref="NotImplementedException"></exception>
        protected virtual void InsertBulk(string tableName, string primaryKeyName, bool autoIncrement, IEnumerable<object> pocos)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// 拼接字符串方法具体实现
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="pd"></param>
        /// <param name="pocos"></param>
        protected virtual void InsertBatch<T>(PocoData pd, IEnumerable<T> pocos)
        {
            PocoData pd2 = pd;
            try
            {
                string primaryKeyName = pd2.TableInfo.PrimaryKey;
                bool autoIncrement = pd2.TableInfo.AutoIncrement;
                IProvider _provider = Database.Provider;
                List<object> args = new();
                List<string> batchSql = new();
                Database.BeginTransaction();
                pocos.ToList().ForEach(delegate (T poco)
                {
                    List<string> list = new();
                    List<string> list2 = new();
                    foreach (KeyValuePair<string, PocoColumn> column in pd2.Columns)
                    {
                        if (!column.Value.ResultColumn)
                        {
                            if (autoIncrement && primaryKeyName != null && primaryKeyName.Equals(column.Key, StringComparison.OrdinalIgnoreCase))
                            {
                                string autoIncrementExpression = _provider.GetAutoIncrementExpression(pd2.TableInfo);
                                if (autoIncrementExpression != null)
                                {
                                    list.Add(column.Key);
                                    list2.Add(autoIncrementExpression);
                                }
                            }
                            else
                            {
                                list.Add(_provider.EscapeSqlIdentifier(column.Key));
                                list2.Add(ConvertValue(column.Value.GetValue(poco), args));
                            }
                        }
                    }

                    if (batchSql.Count == 0)
                    {
                        batchSql.Add(GenerateHeaderSql(Database.Provider.EscapeTableName(pd2.TableInfo.TableName), list.ToArray(), list2.ToArray()));
                    }
                    else
                    {
                        batchSql.Add(GenerateBodySql(list2.ToArray()));
                    }

                    if (batchSql.Count == BatchSize)
                    {
                        InsertIntoDb(batchSql, args);
                    }
                });
                if (batchSql.Count > 0)
                {
                    InsertIntoDb(batchSql, args);
                }

                Database.CompleteTransaction();
            }
            catch (Exception ex)
            {
                Database.AbortTransaction();
                throw;
            }
        }

        protected virtual void InsertIntoDb(List<string> sqls, List<object> args)
        {
            Database.Execute(string.Join("", sqls), args.ToArray());
            sqls.Clear();
            args.Clear();
        }

        protected virtual string GenerateHeaderSql(string tableName, string[] names, string[] values)
        {
            DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new(25, 3);
            defaultInterpolatedStringHandler.AppendLiteral("INSERT INTO ");
            defaultInterpolatedStringHandler.AppendFormatted(tableName);
            defaultInterpolatedStringHandler.AppendLiteral(" (");
            defaultInterpolatedStringHandler.AppendFormatted(string.Join(",", names));
            defaultInterpolatedStringHandler.AppendLiteral(") VALUES (");
            defaultInterpolatedStringHandler.AppendFormatted(string.Join(",", values));
            defaultInterpolatedStringHandler.AppendLiteral(")");
            return defaultInterpolatedStringHandler.ToStringAndClear();
        }

        protected virtual string GenerateBodySql(string[] values)
        {
            return ",(" + string.Join(",", values) + ")";
        }

        /// <summary>
        /// 生成数据库插入语句
        /// </summary>
        /// <param name="tableName"></param>
        /// <param name="names"></param>
        /// <param name="values"></param>
        /// <returns></returns>
        protected virtual string GenerateSql(string tableName, string[] names, string[] values)
        {
            DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new(25, 3);
            defaultInterpolatedStringHandler.AppendLiteral("INSERT INTO ");
            defaultInterpolatedStringHandler.AppendFormatted(tableName);
            defaultInterpolatedStringHandler.AppendLiteral(" (");
            defaultInterpolatedStringHandler.AppendFormatted(string.Join(",", names));
            defaultInterpolatedStringHandler.AppendLiteral(") VALUES (");
            defaultInterpolatedStringHandler.AppendFormatted(string.Join(",", values));
            defaultInterpolatedStringHandler.AppendLiteral(")");
            return defaultInterpolatedStringHandler.ToStringAndClear();
        }

        /// <summary>
        /// 数据类型转化方法
        /// </summary>
        /// <param name="val"></param>
        /// <param name="args"></param>
        /// <returns></returns>
        protected virtual string ConvertValue(object? val, List<object> args)
        {
            if (val == null)
            {
                return "NULL";
            }

            Type type = val!.GetType();
            if (type.IsEnum)
            {
                val = Convert.ChangeType(val, Enum.GetUnderlyingType(type));
                type = val!.GetType();
            }

            if (Utils.IsToStringableNumericType(type))
            {
                return val.ToString() ?? "NULL";
            }

            if (val is bool)
            {
                return Database.Provider.MapParameterValue(val).ToString() ?? "False";
            }

            DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new(1, 1);
            defaultInterpolatedStringHandler.AppendLiteral("@");
            defaultInterpolatedStringHandler.AppendFormatted(args.Count);
            string result = defaultInterpolatedStringHandler.ToStringAndClear();
            args.Add(val);
            return result;
        }

        /// <summary>
        /// 批次删除方法
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="pocos"></param>
        public virtual void DeleteBatch<T>(IEnumerable<T> pocos)
        {
            PocoData pd = PocoData.ForType(typeof(T), Database.DefaultMapper);
            DeleteBatch(pd, pocos);
        }

        /// <summary>
        /// 批次删除方法
        /// </summary>
        /// <param name="tableName"></param>
        /// <param name="primaryKeyName"></param>
        /// <param name="pocos"></param>
        public virtual void DeleteBatch(string tableName, string primaryKeyName, IEnumerable<object> pocos)
        {
            PocoData pocoData = PocoData.ForType(pocos.First().GetType(), Database.DefaultMapper);
            if (!string.IsNullOrEmpty(tableName))
            {
                pocoData.TableInfo.TableName = tableName;
            }

            if (!string.IsNullOrEmpty(primaryKeyName))
            {
                pocoData.TableInfo.PrimaryKey = primaryKeyName;
            }

            DeleteBatch(pocoData, pocos);
        }

        /// <summary>
        /// 批次删除方法
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="pd"></param>
        /// <param name="pocos"></param>
        protected virtual void DeleteBatch<T>(PocoData pd, IEnumerable<T> pocos)
        {
            string primaryKey = pd.TableInfo.PrimaryKey;
            IEnumerable<object?>? pks = pocos.ToPrimarys(primaryKey);
            List<object?>? list = null != pks ? pks.ToList() : default;
            if (null == list)
                return;
            try
            {
                Database.BeginTransaction();
                int num = 0;
                int num2 = 100;
                do
                {
                    IEnumerable<object?> ids = list.Skip(num).Take(num2);
                    Database.Delete<T>("where " + primaryKey + " in (@ids)", new object[1] { new { ids } });
                    num += num2;
                }
                while (num < list.Count);
                Database.CompleteTransaction();
            }
            catch (Exception)
            {
                Database.AbortTransaction();
                throw;
            }
        }
    }
}